/* 
  提取自 ahk 文档
 */
import index_data from './data_index.js'

const uri = 'https://www.autohotkey.com/docs/v2/'
const cache = {
  index_data: index_data
}

export default addSyntaxColors

function addSyntaxColors(str) {
  // Create lists of syntax elements by using index data to reduce code size.
  // An index entry counts as syntax element, if its third field is one of the following digits:
  /* 
      0 - directive
      1 - built-in var
      2 - built-in function
      3 - control flow statement
      4 - operator
      5 - declaration
      6 - built-in class
      7 - built-in method/property
      99 - Ahk2Exe compiler
  */
  var syntax = [], entry = '', type = '', els = []
  let out, index
  var assignOp = '(?:&lt;&lt;|<<|&gt;&gt;|>>|\\/\\/|\\^|&amp;|&|\\||\\.|\\/|\\*|-|\\+|:)='

  for (var i = cache.index_data.length - 1; i >= 0; i--) {
    entry = cache.index_data[i][0]
    type = cache.index_data[i][2]
    syntax[type] = syntax[type] || []
    if (typeof type !== 'undefined') {
      if (entry.indexOf(' ... ') != -1) {
        let part = entry.split(' ... ')
        for (let k in part) {
          syntax[type][k] = syntax[type][k] || []
          if (syntax[type][k].indexOf(part[k]) == -1)
            syntax[type][k].push(part[k])
        }
      }
      else
        (syntax[type].single = syntax[type].single || []).push(entry);
      (syntax[type].dict = syntax[type].dict || {})[entry.toLowerCase()] = i
    }
  }

  return process(str)

  function process(_code) {
    let pre = document.createElement('pre')
    pre.innerHTML = _code
    els.order = []
    // Temporary remove elements which interfering with syntax detection:
    els.order.push('various'); els.various = []
    els.order.push('em'); els.em = []

    Array.from(pre.children).forEach(el => {
      if (el.tagName === 'EM') {
        els.em.push(el.outerHTML)
        let emNode = document.createElement('em')
        emNode.innerHTML = el.innerHTML
        el.parentNode.replaceChild(emNode, el)
      }
      else if (el.href && el.href.substring(0, 4) !== 'http') {
        let eNode = document.createElement('a')
        eNode.innerText = el.innerText
        el.parentNode.replaceChild(eNode, el)
      } else if (el.attributes.length > 0 || el.children.length > 0) {
        els.various.push(el.outerHTML)
        let eNode = document.createElement('various')
        eNode.innerHTML = el.innerHTML
        el.parentNode.replaceChild(eNode, el)
      }
    })

    // Store pre content into a variable to improve performance:
    var innerHTML = pre.innerHTML
    // comments:
    els.order.push('sct'); els.sct = []
    innerHTML = innerHTML.replace(/(\s|^)(;.*?)$/gm, function (_, PRE, COMMENT) {
      out = wrap(COMMENT, 'x-ahk-sct', null)
      els.sct.push(out)
      return PRE + '<sct></sct>'
    })
    els.order.push('mct'); els.mct = []
    innerHTML = innerHTML.replace(/(^\s*\/\*[\s\S]*?^\s*(\*\/|$(?![\r\n])))/gm, function (COMMENT) {
      out = wrap(COMMENT, 'x-ahk-mct', null)
      els.mct.push(out)
      return '<mct></mct>'
    })
    // escape sequences:
    els.order.push('esc'); els.esc = []
    innerHTML = innerHTML.replace(/`./gm, function (SEQUENCE) {
      out = wrap(SEQUENCE, 'x-ahk-esc', null)
      els.esc.push(out)
      return '<esc></esc>'
    })
    // function definitions:
    els.order.push('fun'); els.fun = []
    innerHTML = innerHTML.replace(/^(\s*?static\s*?|\s*?)(\S*?)(?=\(.*?\)\s*(<(em|sct)><\/(em|sct)>\s*)*{)/mg, function (ASIS, PRE, DEFINITION) {
      if (DEFINITION.match(/^(while|if)$/i))
        return ASIS
      out = wrap(DEFINITION, 'x-ahk-fun', null)
      els.fun.push(out)
      return PRE + '<fun></fun>'
    })
    // numeric values:
    els.order.push('num'); els.num = []
    innerHTML = innerHTML.replace(/\b((0(x|X)[0-9a-fA-F]*)|(([0-9]+\.?[0-9]*)|(\.[0-9]+))((e|E)(\+|-)?[0-9]+)?)\b/gm, function (_, NUMBER) {
      out = wrap(NUMBER, 'x-ahk-num', null)
      els.num.push(out)
      return '<num></num>'
    })
    // continuation section inside a string "(...)"
    els.order.push('cont1'); els.cont1 = []
    innerHTML = innerHTML.replace(/('|")([</em>\s]*?^\s*\([\s\S]*?^\s*\).*?)\1/gm, function (_, QUOTE, SECTION) {
      out = processStrParam(SECTION)
      els.cont1.push(out)
      return QUOTE + '<cont1></cont1>' + QUOTE
    })
    // continuation section for hotstrings ::(...)
    els.order.push('cont2'); els.cont2 = []
    innerHTML = innerHTML.replace(/(^\s*:.*?:.*?::)([</em>\s]*?^\s*\([\s\S]*?^\s*\).*?)/gm, function (_, PRE, SECTION) {
      out = processStrParam(SECTION)
      els.cont2.push(out)
      return PRE + '<cont2></cont2>'
    })
    // built-in vars:
    els.order.push('biv'); els.biv = []
    innerHTML = innerHTML.replace(new RegExp('\\b(' + syntax[1].single.join('|') + ')\\b', 'gi'), function (_, BIV) {
      out = wrap(BIV, 'x-ahk-biv', 1)
      els.biv.push(out)
      return '<biv></biv>'
    })
    // strings:
    els.order.push('str'); els.str = []
    innerHTML = innerHTML.replace(/(("|')[\s\S]*?\2)/gm, function (_, STRING) {
      out = wrap(STRING, 'x-ahk-str', null)
      index = els.str.push(out) - 1
      return '<str ' + index + '></str>'
    })
    // methods:
    els.order.push('met'); els.met = []
    innerHTML = innerHTML.replace(/(\.)([^~`!@#$%^&*(){}[\];:"'<,.>?/\\|+=\-\s]+?)(?=\()/g, function (_, PRE, METHOD) {
      out = PRE + wrap(METHOD, 'x-ahk-met', null)
      els.met.push(out)
      return '<met></met>'
    })
    // properties:
    els.order.push('prp'); els.prp = []
    innerHTML = innerHTML.replace(/\.([^~`!@#$%^&*(){}[\];:"'<,.>?/\\|+=\-\s]+?)\b/g, function (_, PROPERTY) {
      out = '.' + wrap(PROPERTY, 'x-ahk-prp', null)
      els.prp.push(out)
      return '<prp></prp>'
    })
    // declaration: class ... extends
    els.order.push('dec_cls'); els.dec_cls = []
    innerHTML = innerHTML.replace(/(^\s*)(class)(\s+\S+\s+)(extends)\b/gim, function (_, PRE, CLASS, INPUT, EXTENDS) {
      var link = cache.index_data[syntax[5].dict['class']][1]
      els.dec_cls.push(wrap(CLASS, 'x-ahk-dec', link))
      els.dec_cls.push(wrap(EXTENDS, 'x-ahk-dec', link))
      return PRE + '<dec_cls></dec_cls>' + INPUT + '<dec_cls></dec_cls>'
    })
    // declarations:
    els.order.push('dec'); els.dec = []
    innerHTML = innerHTML.replace(new RegExp('(^\\s*)(' + syntax[5].single.join('|') + ')\\b(?=\\s|$)', 'gim'), function (_, PRE, DEC) {
      out = PRE + wrap(DEC, 'x-ahk-dec', 5)
      els.dec.push(out)
      return '<dec></dec>'
    })
    // directives:
    els.order.push('dir'); els.dir = []
    innerHTML = innerHTML.replace(new RegExp('(' + syntax[0].single.join('|') + ')\\b($|[\\s,])(.*?)(?=<(?:em|sct)></(?:em|sct)>|$)', 'gim'), function (_, DIR, SEP, PARAMS) {
      // Get type of every parameter:
      var types = cache.index_data[syntax[0].dict[DIR.toLowerCase()]][3]
      // Skip param processing if first param is an expression:
      if (types[0] == 'E') {
        out = wrap(DIR, 'x-ahk-dir', 0)
        els.dir.push(out)
        return '<dir></dir>' + SEP + PARAMS
      }
      // Temporary exclude (...), {...} and [...]:
      let sub = []
      PARAMS = PARAMS.replace(/[({[][^({[]*[\]})]/g, function (c) {
        index = sub.push(c) - 1
        return '<sub ' + index + '></sub>'
      })
      // Split params:
      PARAMS = PARAMS.split(',')
      // Detect smart comma handling:
      if (PARAMS.length > types.length) // For the last param of any directive.
        PARAMS.push(PARAMS.splice(types.length - 1).join(','))
      // Iterate params and recompose them:
      for (let n in PARAMS) {
        // Restore (...), {...} and [...] previously excluded:
        PARAMS[n] = PARAMS[n].replace(/<sub (\d+)><\/sub>/g, function (_, index) {
          return sub[index]
        })
        if (PARAMS[n].match(/^\s*%\s/)) // Skip forced expression parameter:
          continue
        if (types[n] == 'S') // string
          PARAMS[n] = processStrParam(PARAMS[n])
      }
      PARAMS = PARAMS.join(',')
      out = wrap(DIR, 'x-ahk-dir', 0) + SEP + PARAMS
      els.dir.push(out)
      return '<dir></dir>'
    })
    // built-in functions:
    els.order.push('bif'); els.bif = []
    innerHTML = innerHTML.replace(new RegExp('\\b(' + syntax[2].single.join('|') + ')\\b(?=$|\\(|\\s(?!\\s*' + assignOp + '))', 'gi'), function (_, BIF) {
      out = wrap(BIF, 'x-ahk-bif', 2)
      els.bif.push(out)
      return '<bif></bif>'
    })
    // built-in classes:
    els.order.push('cls'); els.cls = []
    innerHTML = innerHTML.replace(new RegExp('\\b(' + syntax[6].single.join('|') + ')\\b', 'gi'), function (_, CLS) {
      out = wrap(CLS, 'x-ahk-cls', 6)
      els.cls.push(out)
      return '<cls></cls>'
    })
    // control flow statements:
    els.order.push('cfs'); els.cfs = []
    innerHTML = innerHTML.replace(
      new RegExp('\\b('
        + syntax[3][0].join('|')
        + ')(\\s+(?:\\S+|\\S*(?:\\s*,\\s*\\S*)*)\\s+|\\s+)('
        + syntax[3][1].join('|')
        + ')(\\s+.+?)(?=<(?:em|sct)></(?:em|sct)>|$|{)|\\b('
        + syntax[3].single.join('|')
        + ')\\b($|,|{|(?=\\()|\\s(?!\\s*'
        + assignOp
        + '))(.*?)(?=<(?:em|sct)></(?:em|sct)>|$|{|\\b('
        + syntax[3].single.join('|')
        + ')\\b)', 'gim'),
      function (ASIS, IF, INPUT, BETWEEN, VAL, CFS, SEP, PARAMS) {
        if (IF) {
          if (INPUT) {
            let cfs = cache.index_data[syntax[3].dict[(IF + ' ... ' + BETWEEN).toLowerCase()]]
            if (cfs)
              out = wrap(IF, 'x-ahk-cfs', cfs[1]) + INPUT + wrap(BETWEEN, 'x-ahk-cfs', cfs[1]) + ((cfs[3][1] == 'S')
                ? processStrParam(VAL) : VAL)
            else
              out = ASIS
          }
        }
        else {
          let cfs = CFS.toLowerCase()
          // Skip param processing if the statement uses parentheses:
          if (PARAMS.charAt(0) == '(') {
            out = wrap(CFS, 'x-ahk-cfs', 3)
            els.cfs.push(out)
            return '<cfs></cfs>' + SEP + PARAMS
          }
          // Get type of every parameter:
          var types = cache.index_data[syntax[3].dict[cfs]][3]
          // Temporary exclude (...), {...} and [...]:
          let sub = []
          PARAMS = PARAMS.replace(/[({[][^({[]*[\]})]/g, function (c) {
            index = sub.push(c) - 1
            return '<sub ' + index + '></sub>'
          })
          // Split params:
          PARAMS = PARAMS.split(',')
          // Iterate params and recompose them:
          for (let n in PARAMS) {
            // Restore (...), {...} and [...] previously excluded:
            PARAMS[n] = PARAMS[n].replace(/<sub (\d+)><\/sub>/g, function (_, index) {
              return sub[index]
            })
            if (PARAMS[n].match(/^\s*%\s/)) // Skip forced expression parameter:
              continue
            if (types[n] == 'S') // string
              PARAMS[n] = processStrParam(PARAMS[n])
          }
          PARAMS = PARAMS.join(',')
          out = wrap(CFS, 'x-ahk-cfs', 3) + SEP + PARAMS
        }
        els.cfs.push(out)
        return '<cfs></cfs>'
      })
    // hotstrings:
    els.order.push('hotstr'); els.hotstr = []
    innerHTML = innerHTML.replace(/^(\s*)(:.*?:)(.*?)(::)(.*)/mg, function (_, PRE, HOTSTR1, ABBR, HOTSTR2, REPL) {
      out = PRE + wrap(HOTSTR1, 'x-ahk-lab', null) + wrap(ABBR, 'x-ahk-str', null)
        + wrap(HOTSTR2, 'x-ahk-lab', null) + (HOTSTR1.match(/x/i) ? REPL : wrap(REPL, 'x-ahk-str', null))
      els.hotstr.push(out)
      return '<hotstr></hotstr>'
    })
    // hotkeys:
    els.order.push('hotkey'); els.hotkey = []
    innerHTML = innerHTML.replace(/^(\s*)((([#!^+*~$]|&lt;|&gt;)*(\S+)( up)?|~?(\S+) &amp; ~?(\S+)( up)?)::)/gim, function (_, PRE, HOTKEY) {
      out = PRE + wrap(HOTKEY, 'x-ahk-lab', null)
      els.hotkey.push(out)
      return '<hotkey></hotkey>'
    })
    // labels:
    els.order.push('lab'); els.lab = []
    innerHTML = innerHTML.replace(/^(\s*)([^\s{(]+?:)(?=\s*(<(em|sct)><\/(em|sct)>|$))/mg, function (_, PRE, LABEL) {
      out = PRE + wrap(LABEL, 'x-ahk-lab', null)
      els.lab.push(out)
      return '<lab></lab>'
    })
    // Release changes:
    pre.innerHTML = innerHTML
    // Restore elements:
    for (let k = els.order.length - 1; k >= 0; k--) {
      let elements = pre.querySelectorAll(els.order[k])
      for (let i = 0; i < elements.length; i++) {
        let element = elements[i]
        if (element.attributes[0]) {
          element.outerHTML = els[els.order[k]][element.attributes[0].name]
        } else {
          element.outerHTML = els[els.order[k]][i]
        }
      }
    }
    return pre.innerHTML
  }

  function wrap(match, className, TypeOrLink) {
    var span = document.createElement('span')
    span.className = className
    if (TypeOrLink != null) {
      var a = document.createElement('a')
      if (typeof TypeOrLink == 'number')
        a.href = uri + cache.index_data[syntax[TypeOrLink].dict[match.toLowerCase()]][1]
      else
        a.href = uri + TypeOrLink
      a.innerHTML = match
      span.appendChild(a)
    } else
      span.innerHTML = match
    return span.outerHTML
  }
  function processStrParam(param) {
    if (param.match(/^\s*(\+|-|\^)?\s*<num><\/num>\s*$/)) // skip number
      return param
    param = param.replace(/<str (\d+)><\/str>/g, function (_, index) { // resolve substring
      return els.str[index]
    })
    // handle %...%:
    let out = '', lastIndex = 0
    let re = /%[^,\s]+?%/g
    let m
    while ((m = re.exec(param))) {
      out += wrap(param.slice(lastIndex, m.index), 'x-ahk-str', null) + m[0]
      lastIndex = re.lastIndex
    }
    out += wrap(param.slice(lastIndex), 'x-ahk-str', null)
    return out
  }
};